package com.cocotingo.snail.text;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.cocotingo.snail.GraphicsX;
import com.cocotingo.snail.Vector;
import com.cocotingo.snail.View;

public class RichTextView extends View {

	private Color defaultColor;
	private Font defaultFont;
	private RichTextBlock[] blocks;
	private final ArrayList<LocatedRichTextBlock> renderingBlocks;
	final ArrayList<Hotspot> hotspots;
	private int lineSpace;
	private int preferredHeight;
	private int linebreakMode;
	
	public static final int LINEBREAK_MODE_DEFAULT = 0;
	public static final int LINEBREAK_MODE_DISABLE = 1;
	
	private class LocatedRichTextBlock extends RichTextBlock {
		
		Vector location;

		public LocatedRichTextBlock(RichTextBlock block) {
			super(block);
			location = new Vector();
		}
		
	}
	
	public RichTextView() {
		this(0, 0, 0, 0);
	}
	
	public RichTextView(int left, int top, int width, int height) {
		super(left, top, width, height);
		defaultColor = Color.black;
		defaultFont = TextView.DEFAULT_FONT;
		blocks = new RichTextBlock[0];
		renderingBlocks = new ArrayList<RichTextView.LocatedRichTextBlock>();
		hotspots = new ArrayList<Hotspot>();
		lineSpace = 0;
		linebreakMode = LINEBREAK_MODE_DEFAULT;
	}
	
	public int getLineSpace() {
		return lineSpace;
	}
	
	public void setLineSpace(int lineSpace) {
		this.lineSpace = lineSpace;
		rebuild();
	}
	
	public Color getDefaultColor() {
		return defaultColor;
	}
	
	public Font getDefaultFont() {
		return defaultFont;
	}
	
	public void setDefaultColor(Color defaultColor) {
		this.defaultColor = defaultColor;
		rebuild();
	}
	
	public void setDefaultFont(Font defaultFont) {
		this.defaultFont = defaultFont;
		rebuild();
	}
	
	@Override
	public void setSize(int width, int height) {
		if (width != getWidth()) {
			super.setSize(width, height);
			rebuild();
		} else {
			super.setSize(width, height);
		}
	}
	
	public int getLinebreakMode() {
		return linebreakMode;
	}
	
	public void setLinebreakMode(int linebreakMode) {
		this.linebreakMode = linebreakMode;
		rebuild();
	}
	
	private class BlockBuffer {
		LinkedList<RichTextBlock> buf = new LinkedList<RichTextBlock>();
		int ptr = 0;
		void push(RichTextBlock block) {
			buf.push(block);
		}
		RichTextBlock pop() {
			if (buf.size() > 0) {
				return buf.pop();
			} else if (ptr < RichTextView.this.blocks.length) {
				return RichTextView.this.blocks[ptr++];
			} else {
				return null;
			}
		}
	}
	
	private void rebuild() {
		renderingBlocks.clear();
		hotspots.clear();
		renderingBlocks.ensureCapacity(blocks.length);
		preferredHeight = 0;
		final BlockBuffer buf = new BlockBuffer();
		final LinkedList<RichTextBlock> line = new LinkedList<RichTextBlock>();
		final GraphicsX g = getTemporaryGraphicsContext();
		g.setFont(defaultFont);
		
		int curLeft = 0;
		int curTop = 0;
		
		// get a block from buffer
		RichTextBlock block = null;
		while ((block = buf.pop()) != null) {
			if (block.getFont() != null) g.setFont(block.getFont());
			
			int block_width = g.getFontMetrics().stringWidth(block.getText());
			if (linebreakMode == LINEBREAK_MODE_DISABLE ||
				curLeft + block_width <= getWidth()) {
				// put it in line
				line.add(block);
				curLeft += block_width;
			} else {
				// need split
				RichTextBlock s_block = splitBlock(g, block, buf, getWidth() - curLeft);
				if (s_block != null) {
					line.add(s_block);
				} else if (line.size() == 0) {
					line.add(block);
				} else {
					buf.push(block);
				}
				// new line
				curLeft = 0;
				curTop = createNewLine(g, line, curTop);
			}
			
			if (block.getFont() != null) g.setFont(defaultFont);
		}
			
		// last line
		curLeft = 0;
		if (line.size() > 0) curTop = createNewLine(g, line, curTop);
		preferredHeight = curTop - lineSpace;
		
		g.dispose();
		setNeedsRepaint();
	}
	
	public int getPreferredHeight() {
		return preferredHeight;
	}
	
	public int getPreferredWidth() {
		GraphicsX g = getTemporaryGraphicsContext();
		int width = 0;
		for (int i = 0; i < blocks.length; i++) {
			Font f = blocks[i].getFont();
			if (f== null) f = getDefaultFont();
			g.setFont(f);
			width += g.getFontMetrics().stringWidth(blocks[i].getText());
		}
		g.dispose();
		return width;
	}
	
	public static final Pattern LINE_BREAKER_PATTERN = Pattern.compile("[\\s]*[^\\s]*");
	
	private RichTextBlock splitBlock(GraphicsX g, RichTextBlock block, BlockBuffer buf, int maxWidth) {
		if (block.isHotspot()) return null;
		
		FontMetrics metrics = g.getFontMetrics(block.getFont() == null ? defaultFont : block.getFont());
		int lastMatchedIndex = 0;
		Matcher m = LINE_BREAKER_PATTERN.matcher(block.getText());
		while (m.find(lastMatchedIndex)) {
			String substring = block.getText().substring(0, m.end());
			if (metrics.stringWidth(substring) <= maxWidth) {
				lastMatchedIndex = m.end();
			} else {
				break;
			}
		}
		
		if (lastMatchedIndex == 0) return null;
		else {
			String substring1 = block.getText().substring(0, lastMatchedIndex);
			String substring2 = block.getText().substring(lastMatchedIndex);
			for (int i = 0; i < substring2.length(); i++) {
				if (!Character.isWhitespace(substring2.charAt(i))) {
					substring2 = substring2.substring(i);
					break;
				}
			}
			RichTextBlock b1 = new RichTextBlock(block);
			RichTextBlock b2 = new RichTextBlock(block);
			b1.setText(substring1);
			b2.setText(substring2);
			buf.push(b2);
			return b1;
		}
	}
	
	private int createNewLine(GraphicsX g, LinkedList<RichTextBlock> line, int curTop) {
		FontMetrics metrics = g.getFontMetrics(defaultFont);
		int maxAscent = metrics.getAscent();
		int maxDescent = metrics.getDescent();
		for (RichTextBlock b : line) {
			if (b.getFont() != null) {
				metrics = g.getFontMetrics(b.getFont());
				maxAscent = maxAscent > metrics.getAscent() ? maxAscent : metrics.getAscent();
				maxDescent = maxDescent > metrics.getDescent() ? maxDescent : metrics.getDescent();
			}
		}
		
		int curLeft = 0;
		for (RichTextBlock b : line) {
			metrics = g.getFontMetrics(b.getFont() == null ? defaultFont : b.getFont());
			LocatedRichTextBlock newBlock = new LocatedRichTextBlock(b);
			newBlock.location.x = curLeft;
			int cur_width = metrics.stringWidth(b.getText());
			curLeft += cur_width;
			newBlock.location.y = curTop + maxAscent;
			renderingBlocks.add(newBlock);
			if (newBlock.isHotspot()) {
				Rectangle frame = new Rectangle(newBlock.location.x, newBlock.location.y - maxAscent, cur_width, maxAscent + maxDescent);
				hotspots.add(new Hotspot(frame, newBlock.getText(), newBlock.getFont() == null ? defaultFont : newBlock.getFont(), newBlock.getColor()));
			}
		}
		
		line.clear();
		curLeft = 0;
		curTop += maxAscent + maxDescent + lineSpace;
		return curTop;
	}
	
	public void setText(RichTextBlock[] blocks) {
		this.blocks = new RichTextBlock[blocks.length];
		for (int i = 0; i < blocks.length; i++) {
			this.blocks[i] = new RichTextBlock(blocks[i]);
		}
		rebuild();
	}
	
	public void setText(RichTextView view) {
		setText(view.blocks);
	}
	
	private boolean antialiased = true;
	
	public boolean isAntialiased() {
		return antialiased;
	}
	
	public void setAntialiased(boolean antialiased) {
		this.antialiased = antialiased;
		setNeedsRepaint();
	}
	
	@Override
	protected void repaintView(GraphicsX g) {
		if (!antialiased) {
			g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		}
		for (LocatedRichTextBlock block : renderingBlocks) {
			g.setColor(block.getColor() == null ? defaultColor : block.getColor());
			g.setFont(block.getFont() == null ? defaultFont : block.getFont());
			g.drawString(block.getText(), block.location.x, block.location.y);
		}
	}
	
	public int getHotspotCount() {
		return hotspots.size();
	}
	
	public Hotspot getHotspot(int index) {
		return new Hotspot(hotspots.get(index));
	}
	
	public int isInsideHotspot(Vector v) {
		for (int i = 0; i < hotspots.size(); i++) {
			Rectangle r = hotspots.get(i).frame;
			if (v.x >= r.position.x && v.x < r.position.x + r.size.x &&
			v.y >= r.position.y && v.y < r.position.y + r.size.y) return i;
		}
		return -1;
	}
	
	@Override
	public String toString() {
		StringBuffer buf = new StringBuffer();
		if (blocks != null) {
			for (RichTextBlock block : blocks) {
				buf.append(block);
			}
		}
		return buf.toString();
	}

}
